#include "njm_renderer.hpp"
#include <iostream>
#include <stdexcept>
#include <array>

namespace njm {
    
    NjmRenderer::NjmRenderer(NjmWindow& window, NjmDevice& device): njmWindow{window}, njmDevice{device}
    {
        recreateSwapChain();
        createCommandBuffers();
    }
    NjmRenderer::~NjmRenderer() { freeCommandBuffers(); }
  
    void NjmRenderer::createCommandBuffers()
    {
        commandBuffers.resize(NjmSwapChain::MAX_FRAMES_IN_FLIGHT);
        VkCommandBufferAllocateInfo allocInfo{};
        allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
        allocInfo.commandPool = njmDevice.getCommandPool();
        allocInfo.commandBufferCount = static_cast<uint32_t>(commandBuffers.size());
        if (vkAllocateCommandBuffers(njmDevice.device(), &allocInfo, commandBuffers.data()) != VK_SUCCESS)
        {throw std::runtime_error("NjmRenderer::createCommandBuffers : failed to create cmdbuf");}
    }

    void NjmRenderer::freeCommandBuffers()
    {
        vkFreeCommandBuffers(njmDevice.device(), njmDevice.getCommandPool(), static_cast<uint32_t>(commandBuffers.size()), commandBuffers.data());
        commandBuffers.clear();
    }

    void NjmRenderer::recreateSwapChain()
    {
        auto extent = njmWindow.getExtent();
        while (extent.width == 0 || extent.height == 0)
        {
            extent = njmWindow.getExtent();
            glfwWaitEvents();
        }
        vkDeviceWaitIdle(njmDevice.device());

        if (njmSwapChain == nullptr) {
            njmSwapChain = std::make_unique<NjmSwapChain>(njmDevice, extent);
        } else {
            std::shared_ptr<NjmSwapChain> oldSwapChain = std::move(njmSwapChain);
            njmSwapChain = std::make_unique<NjmSwapChain>(njmDevice, extent, oldSwapChain);
            if(!oldSwapChain->compareSwapFormats(*njmSwapChain.get())) {
                throw std::runtime_error("NjmRenderer::recreateSwapChain : swapchain image or depth format has changed");
            }
        }       
    }

    VkCommandBuffer NjmRenderer::beginFrame()
    {
        std::cout << "--NjmRenderer::beginFrame--" << std::endl;
        assert(!isFrameStarted && "can't call beginFrame while it already started");
        std::cout << "--NjmRenderer::beginFrame: currentFrameIndex : " << currentFrameIndex << std::endl;
        njmSwapChain->setCurrentFrameIndex(currentFrameIndex);
        auto rst = njmSwapChain->aquireNextImage(&currentImageIndex);
        if (rst == VK_ERROR_OUT_OF_DATE_KHR)
        {
            recreateSwapChain();
            return nullptr;
        }       
        if (rst != VK_SUCCESS && rst != VK_SUBOPTIMAL_KHR)
        {throw std::runtime_error("NjmRenderer::drawFrame : failed to aquire swapchain image");}
        std::cout << "--NjmRenderer::beginFrame: get next-img-index : " << currentImageIndex << std::endl;
        
        isFrameStarted = true;
        auto cmdBuf = getCurrentCommandBuffer();
        vkResetCommandBuffer(cmdBuf, VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT);
        VkCommandBufferBeginInfo beginInfo{};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        if (vkBeginCommandBuffer(cmdBuf, &beginInfo) != VK_SUCCESS)
        {throw std::runtime_error("NjmRenderer::recordCommandBuffer : failed to begin cmdbuf");}
        std::cout << "--NjmRenderer::beginFrame: vkBeginCommandBuffer succeed" << std::endl;
        return cmdBuf;
    }

    void NjmRenderer::endFrame()
    {
        assert(isFrameStarted && "can't call endFrame before start it");
        auto cmdBuf = getCurrentCommandBuffer();
        if (vkEndCommandBuffer(cmdBuf) != VK_SUCCESS)
        {throw std::runtime_error("NjmRenderer::recordCommandBuffer : failed to end cmdbuf");}

        // rst = vkQueueSubmit(queue, submitCnt, vksubmitinfo, fence);
        auto rst = njmSwapChain->submitCommandBuffers(&cmdBuf, &currentImageIndex);
        if (rst == VK_ERROR_OUT_OF_DATE_KHR || rst == VK_SUBOPTIMAL_KHR || njmWindow.wasWindowResized())
        {
            njmWindow.resetWindowResizedFlag();
            recreateSwapChain();
        } 
        else if (rst != VK_SUCCESS)
        {throw std::runtime_error("NjmRenderer::drawFrame : failed to submit cmdbuf and present swapchain image");}
        
        isFrameStarted = false;
        currentFrameIndex = (currentFrameIndex + 1) % NjmSwapChain::MAX_FRAMES_IN_FLIGHT;
    }

    void NjmRenderer::beginSwapChainRenderPass(VkCommandBuffer commandBuffer)
    {
        assert(isFrameStarted && "can't begin render-pass before start frame");
        assert(commandBuffer == getCurrentCommandBuffer() &&
            "can't begin render-pass on cmdbuf from a different frame");
        
        VkRenderPassBeginInfo renderPassBeginInfo{};
        renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        renderPassBeginInfo.renderPass = njmSwapChain->getRenderPass();
        renderPassBeginInfo.framebuffer = njmSwapChain->getFrameBuffer(currentImageIndex);
        renderPassBeginInfo.renderArea.offset = {0, 0};
        renderPassBeginInfo.renderArea.extent = njmSwapChain->getSwapChainExtent();
        std::array<VkClearValue, 2> clearValues{};
        clearValues[0].color = {0.01f, 0.01f, 0.01f, 1.0f};
        clearValues[1].depthStencil = {1.0f, 0};
        renderPassBeginInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
        renderPassBeginInfo.pClearValues = clearValues.data();
        vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
        
        VkViewport vp{};
        vp.x = 0.0f;
        vp.y = 0.0f;
        vp.width = static_cast<float>(njmSwapChain->getSwapChainExtent().width);
        vp.height = static_cast<float>(njmSwapChain->getSwapChainExtent().height);
        vp.maxDepth = 1.0f;
        vp.minDepth = 0.0f;
        VkRect2D sc{};
        sc.offset = {0, 0};
        sc.extent = njmSwapChain->getSwapChainExtent();
        vkCmdSetViewport(commandBuffer, 0, 1, &vp);
        vkCmdSetScissor(commandBuffer, 0, 1, &sc);
    }

    void NjmRenderer::endSwapChainRenderPass(VkCommandBuffer commandBuffer)
    {
        assert(isFrameStarted && "can't end render-pass before start frame");
        assert(commandBuffer == getCurrentCommandBuffer() && "can't end render-pass on cmdbuf from a different frame");
        vkCmdEndRenderPass(commandBuffer);
    }

}